YownYang's blog

知识点杂记

这是一篇关于技术知识点的整理汇总,在技术道路上,有许多知识点属于不常用但有用的知识,如果不加以记录,时间久了记忆就会模糊,不利于知识积累。我就是深受其害o(╥﹏╥)o

编程思想与框架

面向过程/面向对象/响应式编程

Q: 面向过程思想

A: 一切以过程为核心去解决问题的语言。将解决问题的方法视为函数,按解决问题的步骤去调用函数,就组成了过程。面向过程适合解决细化的问题。

Q: 面向对象思想

A: 一切以对象为核心去解决问题的语言。对象作为实体,通过对对象进行数据抽象和行为抽象得到了类;通过封装隐藏了内部实现;通过多态实现了动态分配;通过继承实现了特殊化和基本化(这两个我没找到好的形容词)。面向对象适合解决复杂的、静态的问题。

Q: 响应式编程

A: 响应式编程是以关系为核心去解决问题的语言。在现实世界,当关系产生了变化,你可以通过电话、短信任意的联系方式去获取状态变化,以及做出自己的反应。在计算机世界,它的传递方式是数据流。接受数据流的可以是对象,可以是函数。响应式编程适合解决动态的问题。

Q: 编程思想的理解

A: 一切编程思想都是对解决现实问题的某种思考方式,依据这种思考方式去解决问题就是编程思想。

MVC、MVVM、MVP

Q: MVC框架构成

A: Model(数据)/View(视图)/C(控制器)。V和M可以直接通信,所以M中存在数据处理的逻辑,V中存在数据展示的逻辑,C用来接收事件,向M传送数据等其它逻辑。

Q: MVP框架构成

A: Model(数据)/View(视图)/P(展示器)。V和M不可以直接通信。M只具有简单的数据模型,V只具有简单的控件组成,它们没有逻辑在内。P则会把所有的逻辑在其内去处理,这是它和MVC的不同。

Q: MVVM框架构成

A: Model(数据)/View(视图)/VM(控制器)。V和M不可以直接通信。M只具有简单的数据模型,V只具有简单的控件组成,它们没有逻辑在内。VM会包含所有逻辑,但它的实现是进行data-binding来动态的修改数据,这是它与MVP的不同。

Q: 三者的关系

A: MVC衍生出MVP,MVP衍生出MVVM。三个框架没有高低之分,只有适合与不适合,毕竟它们三者都是为了解耦。是数据、界面、逻辑由谁来负责在发生变化。如果具体到iOS上面,由于官方的原因,C本身就是一个很重的东西,即使M和V会帮助C分担一部分的逻辑。所以在iOS上面,我认为也可以理解为通过不同的手段为C减负,保证代码组织的合理性。

编译原理相关

Q: dSYM文件是什么,作用。

A: 每次编译后都会产生的一个文件,本质是一个符号表,包含内存地址、函数名、文件名、行号。作用是可以查找到指定内存地址对应的函数位置。

Q: 如何防止反编译

A: 本地数据加密、URL加密、网络请求加密、代码混淆、代码逻辑混淆

App相关

Q: App启动过程

1
2
3
4
5
6
7
8
9
10
11
12
1. 加载dyld到内存
2. 加载动态库(包括依赖的库)
3. 加载rebase(重定位内存地址的,比如逻辑地址到物理地址)
4. 加载bind(绑定真实的内存地址)
5. 初始化Objective C Runtime
6. 其它初始化(+load/C的__attribute__((constructor)))方法
7. 执行main函数中的UIApplicationMain函数
8. 创建该函数中第3和4个参数的实例对象
9. 读取info.plst文件并设置一些属性
10. 创建main runloop
11. UIApplicationDelegate对象开始监听
12. 执行`didFinishLaunchingWithOptions`函数,如果设置了启动的storyboard,则加载

Q: App启动优化

1
2
3
4
1. 针对加载动态库步骤,减少私有动态库的数量,因为系统动态库有缓存。
2. 针对Rebase和bind和Runtime可以减少category和合并功能类似的类,全局变量
3. 对于其它初始化,使用initialize代替+load,减少使用__atribute__((constructor))方法。
4. 针对main函数之后的优化,能延迟执行就延迟执行。不能延迟执行就放在后台执行。

Q: App内存分布

A: 内存地址由高到低,栈区,堆区,静态区,常量区,代码区

Q: App框架层级

1
2
3
4
1. Cocoa Touch 提供了UIKit等高级框架
2. Media 提供了Core Graphics、Open GL、Core Animation等媒体框架
3. Core Service 提供了Foundation、CF等基础框架
4. Core OS 硬件和软件之间的桥梁

Q: App性能优化

1
2
3
4
5
6
7
8
9
1. ARC管理内存
2. 缓存机制
3. 延迟加载
4. 保证图片大小与容器大小相同
5. 避免复杂的XIB
6. 权衡渲染方法
7. 重用
8. AutoreleasePool
9. 苹果的Profile工具检测,进一步优化具体代码

Q: tableView性能优化

1
2
3
4
1. 高度缓存
2. cell复用
3. 图片按需加载
4. 大的数据在其他线程操作

Q: 读取一个xib文件的步骤

1
2
3
4
1. 将xib编译后的nib文件读入内存,包括引用的图片资源
2. 对nib文件进行解档,并将各元素根据类型初始化,通过initWithCoder:方法
3. 进行连接,对IBoutlet通过KVC的方式赋值;对IBAction通过addTarget:action:forControlEvents添加方法
4. 给界面对象发送awakeFromNib消息

参考连接

多线程相关

Q: 多线程是什么,作用,缺点

A: 多线程是指多个线程并发执行的技术。作用是用来提升同一时间段内系统的处理效率,也就是作业量。缺点是消耗资源,使用不当容易造成数据竞争,死锁,优先级反转等问题。

Q: 线程是什么,特点

A: CPU执行的最小单元,由线程ID、指令指针、寄存器、堆栈组成,也常被称之为轻型进程。特点是无单独的内存空间,与同一进程下的其他线程共享进程的内存空间;线程crash会导致进程crash。

Q: 单线程、并发、并行

A: 一个CPU在一条线程上执行单一无分叉路径为单线程。一个CPU在多条线程切换执行称为并发,也叫伪.多线程。多个CPU同时执行多条线程称为并行,也叫真.多线程。

Q: 数据竞争/死锁/优先级反转

A: 数据竞争,即资源非互斥状态。如果一个属性被多个进程读没有问题,如果同时有进程写入,那么有些进程就会获得旧数据。因此可以通过加锁或者栅栏来解决。

死锁需要四个条件:资源互斥、请求和保持、循环等待、不可剥夺。破坏任何一个就能解决死锁问题。A和B两个线程访问R1和R2资源,如果各持有一个的话,那么A会等B释放,B会等A释放,就形成了死锁。

优先级反转:A,B,C三个线程,优先级A>B>C。C先运行占用资源R,A然后运行来使用资源R,但R已被使用,A只能等待,这时任务B来执行,由于优先级高于C,所以B先执行,然后C执行,最后才是A执行,这样就产生了优先级反转。解决办法是优先级天花板和优先级继承。优先级天花板是指,任意使用资源R的线程,它的优先级都会被提到这个资源的最高优先级。这样就不会出现优先级反转的问题了。优先级继承是指,当C在使用资源R时,A也要使用资源R,通过判断,如果C的优先级低于A,那么将C的优先级临时提升到A。

Q: pthread、NSThread、NSOperation、GCD

A: 都是多线程问题解决方案,pthread和GCD都是一套C语言API,NSThread和NSOperation是一套OC语言的API。pthread和NSThread对线程直接操作,并且需要程序员去管理生命周期,很少使用。NSOperation和GCD不是直接面向线程操作,而是把要执行的方法提交到指定的block或者对象中,程序员无需管理线程的生命周期,也无需考虑当时CPU的使用情况以及消耗,系统会自动进行分配。NSOperation比GCD更易于管理,很简单就可以实现对执行任务的暂停取消依赖等操作。而GCD功能更颗粒化,适合需求简单的多线程操作。

Q: GCD的同步/异步,并发/串行

1
2
3
4
5
6
7
1. 并行队列 + 异步执行 = 多个任务同时执行 + 子线程执行;
2. 串行队列 + 异步执行 = 多个任务顺序执行 + 子线程执行;
3. 全局队列 + 异步执行 -> 子线程执行+同时执行
4. 主队列 + 异步执行 -> 回到主线程方式二
5. 串行队列 + 同步执行(很少使用) = 任务顺序执行 + 主线程执行
6. 并行队列 + 同步执行(不用该组合) = 任务顺序执行 + 主线程执行
7. 主队列 + 同步执行 -> 死锁(两个任务处于相互等待)

Q: NSOperation/NSOperationQueue

1
2
3
1. 依赖高于优先级
2. 非线程安全
3. maxConcurrentOperationCount设置的是队列中同时执行的个数,不是线程个数

链接

Q: 锁

1
2
3
4
5
6
7
1. 自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的(如OSSpinLock、os_unfair_lock)
2. 互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成(如NSLock、pthread_mutex、@synchronized)
3. 读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现(如pthread_rwlock)
4. 信号量:是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥(dispatch_semaphore)
5. 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行(如NSCondition、NSConditionLock)
6. 递归锁:同一个线程可以加锁N次而不会引发死锁。(如NSRecursiveLock、pthread_mutex的PTHREAD_MUTEX_RECURSIVE类型)
7. 互斥锁是2值信号量;自旋锁不会导致等待线程睡眠;其它锁都会导致等待线程睡眠,包括信号量。

Q: 信号量

1
2
3
1. dispatch_semaphore_create 创建一个指定个数的信号
2. dispatch_semaphore_wait 每次会使信号的个数减一,只要大于0就能顺利执行,否则要等待。
3. dispatch_semaphore_signal 使信号量增加一,一定要和上面的方法成对使用,否则会crash的。

Q: 如何避免多线程的常见问题

A: 选择合适的多线程技术;尽量简化线程之间的关系;尽量指定相同优先级;队列中写操作栅栏,读操作并行。

版本控制

Q: SVN,Git

A: SVN是一个集中式控制系统,管理集中在远程服务器。Git是一个分布式控制系统,每台使用git的电脑都拥有版本库,远程也有一个远程库,用来做数据交换。

Q: SVN的缺点

A: 第一,连接不到远程服务器基本无法工作;第二,每个分支每次提交都是整体的拷贝,占用存储空间。

Q: Git的差异记录方式

A: 整体文件做一个快照,文件有变化,产生一个新的快照;文件无变化,延用上一个快照。会使用文件内容或者目录结构生成一个40位的十六进制的哈希值。

组件化

Q: 如何组件化

A: 首先要明白组件化的实质就是把一个整体的耦合度高的项目给拆分成各种小的模块,拆成了模块之后,为了不让各模块耦合,需要一个中间层去进行模块间的通讯。对于一些基础功能模块和可以通用的模块,如网络请求,帮助类,公共UI库可以以cocoapods的方法去进行管理,便于多项目通用和版本管理。

runloop

Q: runloop的运行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 通知observer即将进入runloop
2. 通知observer即将处理timer
3. 通知observer即将处理source0
4. 处理source0
5. 存在source1,跳至第11步;不存在source1,跳至第6步。
6. 无source1、timer,通知observer runloop即将休眠
7. runloop休眠,等待唤醒
8. 通知observer线程已经唤醒
9. timer唤醒,处理timer;
10. gcd主队列任务唤醒,处理主队列任务;
11. 处理source1
12. 是否满足退出条件,不满足跳回第2步
13. 通知observer,runloop退出

Q: runloop概念

1
2
3
4
5
6
1. runloop是iOS中一个很基础很重要的东西,可以从字面理解为运行循环。一旦被创建,除非所在线程消亡,否则一直存在。主线程的runloop是在main函数中创建的,其余线程需要主动获取runloop才会创建。
2. 每个runloop包含有不同的mode,用来让程序在某个mode下专注于对应的任务,一定程度上避免了资源抢占,例如程序初始化的mode,平时的默认mode,滚动时的mode。
3. 每个mode包含不同的事件source、timer、observer
4. source分为source0和source1两种类型。source0只包含一个回调指针,需要CFRunLoopSourceSignal(source)标记待处理,CFRunLoopWakeUp(runloop)唤醒runloop进行处理;source1比source0多了一个mach_port,它会主动唤醒runloop
5. timer 计时器,当加入到runloop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调
6. observer 每个Observer都包含了一个回调指针,当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化

Q: runloop处理的事件

1
2
3
4
1. 自动释放池的创建和销毁
2. 识别硬件(source1)
3. 识别手势(source0)
4. 界面刷新

block

Q: 什么是block

A: 从表面看,block就是一段封装的代码,可以传递数据。从本质上看,它是闭包在iOS的具现化表现。按维基百科给出的解释,闭包就是一个封闭的作用域可以保存捕捉到的外部变量。从代码看,使用clang rewrite之后的block就是一个结构体,包含一个isa指针,捕捉的变量,实现的C函数,描述结构体。

Q: 为什么添加__block后,变量可以被修改

A: 因为添加之后,捕捉的变量就从一个普通的值引用变为了指针引用,用__block修饰的变量会生成一个结构体,结构体内部存储isa指针、变量、forwarding指针、释放函数、copy函数。

屏幕渲染

Q: 屏幕渲染大致流程

1
2
3
4
5
1. 收到vsync信号
2. CPU计算
3. GPU渲染完成后将其放入帧缓冲区
4. 视频控制器逐行读取帧缓冲区并显示
5. 如果在一个vsync信号内,CPU或者GPU没有提交内容,那么就会丢弃那一帧,保持之前的状态,这就是卡顿的原因

runtime

Q: 什么是runtime

runtime是一套底层的C语言API,是iOS的核心之一,平时写的代码,底层都是基于它实现的。

Q: runtime的作用

将一部分编译和链接时候做的工作,转移到运行时去做

内存管理

Q: 主线程中的所有对象会加入到自动释放池吗?

A: 不会,只有调用了autorelease方法的会。一般就是new/alloc/copy/retain之外的对象会自动调用autorelease。

Q: 主线程中的对象什么时候释放?

A: 如果未被加入自动释放池,在引用计数归0后,内存区域会被标记为可分配,但其中的内容并未清理。如果加入了自动释放池,在自动释放池释放时会执行一次release方法,这个时候如果引用计数为0,内存区域会被标记为可分配。

Q: main函数入口的自动释放池中的对象什么时候释放?

A: 系统在每次runloop开始的时候创建了一个自动释放池,在结束的时候释放这个自动释放池,对象也就是在这里释放的。

Q: 子线程中没有runloop,它的自动释放池什么时候释放?

A: 没有runloop,线程不会一直存在,自动释放池也会在线程结束的时候释放。

Q: 内存区域的释放时机?

A: 所谓的释放只是将其标为可分配,其内的值仍然存在。值的覆盖实际上发生在这块内存区域被重新使用时。

Q: Autorelease原理

1
2
3
4
5
PS: 参阅NSObject.mm第628行-1191行
1. AutoreleasePool本身并无结构,实质是AutoreleasePoolPage
2. AutoreleasePoolPage以双向链表存在,当第一个表存满之后,就创建新表继续存储,每个表最大存储4096字节大小。
3. 它有一些重要从参数和概念:next指向最新加进来的对象的下一个位置,判断是否存满也是靠这个参数的;thread,所在的线程;parent指向前一个page,child指向下一个page;holdPage最后page,coldPage首个page;每次objc_autoreleasePoolPush都会插入一个空的标志,用于在pop时使用,会直接pop到这个标志的位置。
4. 明白了这些参数就很好理解了,你使用自动释放池时,就会创建一个poolpage,然后把池内对象加进来,next指向对象下一个位置;当满了,就创建新的page再加进来,设置两个page的parent和child指向。如果嵌套自动释放池,就嵌套poolpage,设置空标志。释放的时候,直接释放到空标志的地方。

Q: Autorelease对返回值的优化

依赖于TLS机制,这是一块某个线程专有的存储内存,以key-value进行读写。调用objc_autoreleaseReturnValue时将其存储进去,调用objc_retainAutoreleasedReturnValue时,去TLS中读取,如果读取到就不需要retain了。如果一方ARC,一方非ARC,那就需要依赖地址偏移量了。

Q: 关联对象功能、基本内部参数(参考objc-refrences.mm文件)

1
2
3
4
5
功能:为现有类添加属性
1. AssociationsManager提供操作时候的spinlock锁
2. AssociationsHashMap提供存储关联对象和ObjectAssociationMap的hash表
3. ObjectAssociationMap用来存储key和ObjcAssociation
4. ObjcAssociation一个C++的类用来存储关联引用的参数

Q: objc_setAssociatedObject

1
2
3
1. 根据传入的value,以及策略,产生new_value
2. 如果new_value为nil,根据对象地址找到ObjectAssociationMap,通过key找到关联引用,将其置为nil
3. 如果new_value不为nil,根据对象地址找到ObjectAssociationMap,这个map不存在的话就自动创建。map存在的话,就根据key去找对应的ObjcAssociation,将新的关联引用存储,旧的关联引用释放。

Q: objc_getAssociatedObject

1
2
1. 根据对象地址去查找ObjectAssociationMap
2. 根据key在ObjectAssociationMap中查找对应的关联引用,存在就返回ObjcAssociation的value值,不存在返回nil。

Q: objc_removeAssociatedObjects

1
2
3
1. 根据对象地址去查找ObjectAssociationMap
2. 将ObjectAssociationMap所有关联引用存储到vector中
3. 释放所有存储对象

JavaScriptCore框架相关

JSCore框架特性

  1. 自动化的,类型自动转换
  2. 安全的,JS是一门动态类型语言,OC是静态类型语言,它保证了转化过程中不会产生crash,并且JSCore提供的方法是线程安全的
  3. 高保真的,说白了就是两边互不侵犯,写OC就写OC,写JS就写JS

JSCore框架中的几个类

  1. JSContext 提供JS执行环境的类
  2. JSValue 是对JS对象的封装,包括函数,可以进行OC对象和JS对象的转化
  3. JSManagedValue 因为JSValue是对js值得强引用,所以很容易造成内存问题,这样就有了JSManagedValue
  4. JSExport 一个协议,使用这个协议,可以将OC中的对象暴露给JS使用。也可以通过直接给jsContext注入block进行单个方法的注入
  5. JSVirtualMachine 它为JS运行提供了底层资源,有独立的堆栈以及垃圾回收机制。一个进程可以包含多个JSVirtualMachine,一个JSVirtualMachine可以包含多个JSContext。一个JSVirtualMachine下不同JSContext中的JSValue可以互相传递,但是不同的JSVirtualMachine不行。

iOS 算法题

一副牌除掉大小王52张,随机抽取一张扔掉,如何得知扔掉那张牌数值以及花色是多少?

YYBox一个数组存储52张卡牌,一个方法随机删除一张;YYCard就一个数值1-13,一个类型黑红梅芳四种。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 1.初始化牌盒,并且生成52张牌,正常来讲牌的大小和花色都是随机的,当然我这个地方省事并没有随机,反正不影响下面的方式
YYBox *box = [YYBox new];
for (NSUInteger i = 1; i < 14; i++) {
for (NSUInteger j = 0; j < 4; j++) {
@autoreleasepool {
YYCard *card = [YYCard new];
card.number = i;
card.type = kYYCardTypeHeart + j;
[box.cards addObject:card];
}
}
}
// 2. 随机删除一张,你不知道删除的哪一张
[box deleteCard];
// 3. 对剩余51张进行遍历,并得到一个key为1-13,value为花色数组的字典
NSMutableDictionary *valueTypeDictionary = [NSMutableDictionary dictionary];
[box.cards enumerateObjectsUsingBlock:^(YYCard *card, NSUInteger idx, BOOL * _Nonnull stop) {
NSNumber *key = @(card.number);
NSMutableArray *typeArray = valueTypeDictionary[key];
if (!typeArray) {
typeArray = [NSMutableArray array];
}
[typeArray addObject:@(card.type)];
[valueTypeDictionary setObject:typeArray forKey:key];
}];
// 4. 遍历字典,当某个数组个数小于4,就是说明它是缺失的牌
[valueTypeDictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, NSMutableArray *typeArray, BOOL * _Nonnull stop) {
if (typeArray.count < 4) {
NSMutableArray *tempArray = [NSMutableArray arrayWithObjects:@(1), @(2), @(3), @(4), nil];
[tempArray removeObjectsInArray:typeArray];
NSLog(@"Dismiss card type is %@, number is %@", [tempArray firstObject], key);
}
}];

问题:一个农场有一只奶牛,第二年和第四年会生育小牛,第五年死亡。生育的小牛同样如此,不考虑公牛,N年后农场多少只牛?

YYFarm只要一个牛的数组;YYCow一个牛的年龄,一个牛年龄加1的函数,这个函数会返回三种结果类型,生小牛,死,无.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 初始化农场,此时农场有一只牛
YYFarm *farm = [YYFarm new];
YYCow *rootCow = [YYCow new];
[farm.cows addObject:rootCow];
// 2. N年后农场多少只牛,假设N是5年,如果N没值,你直接while true好了,但肯定内存爆掉
NSUInteger N = 1;
while (N < 6) {
NSUInteger count = farm.cows.count;
NSMutableArray *tempNewArray = [NSMutableArray array];
NSMutableArray *tempDieArray = [NSMutableArray array];
for (NSUInteger i = 0; i < count; i++) {
YYCow *cow = [farm.cows objectAtIndex:i];
// 每次执行一次模拟过一年,当age等于2或4时,生产一只牛;等于5时,死亡
YYCowRunOneYearResult result = [cow runOneYear];
if (result == kYYCowRunOneYearResultNew) {
YYCow *newCow = [YYCow new];
[tempNewArray addObject:newCow];
} else if (result == kYYCowRunOneYearResultDie) {
[tempDieArray addObject:cow];
}
}
[farm.cows addObjectsFromArray:tempNewArray];
[farm.cows removeObjectsInArray:tempDieArray];
N += 1;
}
NSLog(@"%@ year %@ cow", @(N), @(farm.cows.count));

冷门小知识

Q: ==,isEqual,各类衍生的isEqualXXX的区别

==直接判断,判断基本类型时,判断值是否相等。判断对象时,判断所指向内存地址是否相同。

isEqualXXX,由isEqual衍生而来,当你知道两者类型相同时,使用这个比isEqual快。应该是省去了二者类型的比较和指向地址的比较,直接进行内容比较。

isEqual,NSObject提供的方法,正常来讲是跟==相同。但是iOS中的一些类对它进行了重写,我知道的有UIColor,NSArray,NSDictionary,NSSet,NSData.重写之后它们只进行类型判断和存储的数据判断,而不判断指向地址。不信的话,你可以初始化两个NSObject进行判断,再初始化两个NSArray进行判断,看下返回结果。

最后提一嘴,值相同的,hash值也相同,反之不一定。

Q: App中使用了单独hash表的有

1
2
3
1. 引用计数 (对象地址为key,强引用为value)
2. 弱引用 (对象地址为key,弱引用为value)
3. 关联对象 (对象地址为key,关联引用为value)